module jinji.link.service;

import jinji.common.type,
jinji.link.model,
jinji.rpc,
jinji.util.crypto,
jinji.util.io,
tame.builtins;

debug import tame.logger;

/// clean dead links
int clean() => db.delWhere!(Link, "status=0");

/// 链接还原
string normalizeLink(string url, Id uid = Id.empty) {
	import jinji.util.string,
	tame.string;
	import std.uri : decode;

	version (withRE) {
		import std.regex;

		// TODO: 仅处理用户可见的别名
		foreach (row; query!(SB.select!(LinkAlias.pattern, LinkAlias.substitute))) {
			const pattern = row.get!(string, 0);
			const substitute = row.get!(string, 1);
			url = url.replaceAll(regex(pattern), substitute);
		}
	}
	if (likely(url.length > 7)) {
		if (url.isFileURL) {
			try {
				return decode(url);
			} catch (Exception e) {
				debug warning("URL解码失败：", e);
				return url;
			}
		}
		const proto = url[0 .. 7];
		if (!proto.canFind(':'))
			return "https://" ~ url;
	}
	return url;
}

/// verifier
private auto recvData(void* data, size_t size, size_t nmemb, void* hasher) {
	import blake3.binding;

	auto realsz = size * nmemb;
	blake3_hasher_update(cast(blake3_hasher*)hasher, data, realsz);
	return realsz;
}

/++
验证链接有效性
Params:
	url = 链接
	length = 预期长度
	timeout = 超时时间（秒）
Returns: 链接状态
+/
LinkStatus verifyValidity(string url, size_t length = 0, long timeout = 8) {
	Curl c = initCurl(url, timeout);
	c.nobody = true;
	const code = c.perform();
	if (code != CurlError.ok)
		return LinkStatus.invalid;
	return c.contentLength == length ? LinkStatus.pending : LinkStatus.invalid;
}

/++
获取链接BLAKE3
Params:
	url = 链接
	length = 长度
	timeout = 超时时间（秒）
Returns: BLAKE3
+/
Hash getB3sum(string url, @restrict ref size_t length, long timeout = 8) {
	import tame.string,
	tame.io.file;

	if (url.startsWith("file:///")) {
		auto file = File(url[8 .. $]);
		scope (exit)
			file.close();
		length = 0;
		if (file.size <= 0)
			return emptyHash;

		auto hasher = BLAKE3();
		ubyte[1024] buf = void;
		while (true) {
			const data = file.read(buf);
			if (!data.length)
				break;
			hasher.put(data);
			length += data.length;
		}
		return hasher.finish();
	}
	Curl c = initCurl(url, timeout);
	auto hasher = BLAKE3();
	c.set(CurlOption.writedata, &hasher);
	c.set(CurlOption.writefunction, &recvData);
	const code = c.perform();
	if (code != CurlError.ok) {
		length = 0;
		return emptyHash;
	}
	length = c.contentLength;
	return hasher.finish();
}

/++
验证链接BLAKE3
Params:
	url = 链接
	blake3 = 预期BLAKE3
	length = 预期长度
	timeout = 超时时间（s）
Returns: 链接状态
+/
LinkStatus verifyB3sum(string url,
	const Hash blake3, size_t length = 0, long timeout = 8L)
	=> getB3sum(url, length, timeout) == blake3 ? LinkStatus.active : LinkStatus.invalid;

/++
抽样检测
Params:
	time = 时间戳
	fcount = 有效链接抽样数
	ncount = 无效链接抽样数
Returns: 更新行数
+/
auto sampling(StdTime time, uint fcount = 15, uint ncount = 20) {
	ulong rows;
	foreach (q; query!(SB.select!(Link.url, Link.hash, Link.size)
			.where("type=0 AND status=1 AND updated_at<$1").orderBy("random()")
			.limit("$2"))(time, fcount)) {
		auto url = q.get!string;
		auto status = verifyB3sum(url,
			q.get!(Hash, 1),
			q.get!(long, 2));
		rows += exec!(SB.update!Link.set("status=$1,updated_at=now()")
				.where("url=$2"))(status, url);
	}
	foreach (q; query!(SB.select!(Link.url, Link.size)
			.where("type=0 AND (status=2 OR status=3) AND updated_at<$1")
			.orderBy("random()").limit("$2"))(time, ncount)) {
		auto url = q.get!string;
		auto status = verifyValidity(url, q.get!(long, 2));
		rows += exec!(SB.update!Link.set("status=$1,updated_at=now()")
				.where("url=$2"))(status, url);
	}
	return rows;
}
